{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 自定义 Remote Tool\n",
    "\n",
    "当前企业中大部分的功能都是通过 API 的形式暴露，Agent如果想要拓展自己的能力边界，就必须基于现有的功能性 API（eg：查天气或查火车票的 api）来进行交互，从而实现更复杂的企业级功能。\n",
    "\n",
    "Agent想与存量功能性 API 进行交互需要有一个标准的交互协议，而 ERNIE-Bot-Agent 中已经提供了 RemoteTool 和 RemoteToolkit 来简化此交互流程，接下来将介绍 如何在 ERNIE-Bot-Agent 中使用 RemoteTool。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 使用 RemoteTool\n",
    "\n",
    "RemoteTool（远程工具）可以是 AI Studio 的工具中心提供，可以是开发者自己提供，而形式可以有两种：\n",
    "\n",
    "1. 现有 RESTful API\n",
    "2. 基于 EB Agent 开发 RemoteTool\n",
    "\n",
    "### RESTful API\n",
    "\n",
    "现在大量的 Web 应用几乎绝大部分基于 RESTful API构建，所以有效利用现有 RESTful API 扩展 Agent 能力边界能够极大的降低开发成本。\n",
    "\n",
    "在开始本教程前，我们需要先获取[飞桨AI Studio星河社区的access_token](https://aistudio.baidu.com/index/accessToken)并且其配置成环境变量，用于对调用大模型和工具中心进行鉴权。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "import os\n",
    "os.environ[\"EB_AGENT_ACCESS_TOKEN\"] = \"<access_token>\"\n",
    "\n",
    "os.environ[\"EB_AGENT_LOGGING_LEVEL\"] = \"info\"\n",
    "\n",
    "from IPython import get_ipython\n",
    "get_ipython().system = os.system"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在此通过 FastAPI 开发一个单词本的 RESTFul API 服务为例来展开：\n",
    "\n",
    "#### 安装依赖"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: fastapi in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (0.108.0)\n",
      "Requirement already satisfied: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (2.5.3)\n",
      "Requirement already satisfied: starlette<0.33.0,>=0.29.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (0.32.0.post1)\n",
      "Requirement already satisfied: typing-extensions>=4.8.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from fastapi) (4.9.0)\n",
      "Requirement already satisfied: annotated-types>=0.4.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (0.6.0)\n",
      "Requirement already satisfied: pydantic-core==2.14.6 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi) (2.14.6)\n",
      "Requirement already satisfied: anyio<5,>=3.4.0 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from starlette<0.33.0,>=0.29.0->fastapi) (4.2.0)\n",
      "Requirement already satisfied: idna>=2.8 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (3.6)\n",
      "Requirement already satisfied: sniffio>=1.1 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (1.3.0)\n",
      "Requirement already satisfied: exceptiongroup>=1.0.2 in /Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages (from anyio<5,>=3.4.0->starlette<0.33.0,>=0.29.0->fastapi) (1.2.0)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "!pip install fastapi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 开发 FastAPI 的web 服务\n",
    "\n",
    "FastAPI 可以通过 pydantic 来定一输入和输出的数据格式，同时还能够自动生成 OpenAPI.yaml 文件提供给 RemoteToolkit 来解析。\n",
    "\n",
    "当然如果开发是基于其他 web 框架（跟编程语言没有关系）开发也是可以的，只要提供了标准的 OpenAPI 3.0 的文件即可。\n",
    "\n",
    "Web 服务代码如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Started server process [19881]\n",
      "INFO:     Waiting for application startup.\n",
      "INFO:     Application startup complete.\n",
      "INFO:     Uvicorn running on http://0.0.0.0:8020 (Press CTRL+C to quit)\n"
     ]
    }
   ],
   "source": [
    "from fastapi import FastAPI\n",
    "from erniebot_agent.tools.schema import ToolParameterView, Field\n",
    "import uvicorn\n",
    "from threading import Thread\n",
    "\n",
    "app = FastAPI()\n",
    "prompt = '请避免使用\"根据提供的内容、文章、检索结果……\"等表述，不要做过多的解释。'\n",
    "\n",
    "\n",
    "class AddWordInput(ToolParameterView):\n",
    "    word: str = Field(description=\"待添加的单词\")\n",
    "\n",
    "\n",
    "wordbook = []\n",
    "\n",
    "\n",
    "@app.post(\"/add_word\", description=\"在单词本中添加一个单词\")\n",
    "async def add_word(word_input: AddWordInput):\n",
    "    if word_input.word in wordbook:\n",
    "        return {\"message\": f\"单词：“{word_input.word}” 已存在\"}\n",
    "\n",
    "    wordbook.append(word_input.word)\n",
    "    return {\"message\": \"单词添加成功\", \"prompt\": prompt}\n",
    "\n",
    "\n",
    "@app.get(\"/get_words\", description=\"获取单词本中的内容\")\n",
    "async def get_words():\n",
    "    return {\"words\": wordbook, \"prompt\": prompt}\n",
    "\n",
    "\n",
    "@app.get(\"/.well-known/openapi.yaml\")\n",
    "async def get_openapi_yaml():\n",
    "    \"\"\"这块可以返回本地 openapi.yaml 文件也是 ok 的\"\"\"\n",
    "    return app.openapi()\n",
    "\n",
    "\n",
    "thread = Thread(target=uvicorn.run, kwargs={\"app\": app, \"host\": \"0.0.0.0\", \"port\": 8020})\n",
    "thread.daemon = True\n",
    "thread.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以上代码展示了如何使用 FastAPI 开发本地 RESTFul API 服务，**开发者可以将上述的服务替换成自己的业务服务**，并需要提供：`/.well-known/openapi.yaml` URL：提供服务描述文件，尽可能详细。\n",
    "\n",
    "> `openapi.yaml` 文件为 API 的描述文件，提供了每个 API 的描述信息、输入输出格式、API 路径以及执行方式等，Agent 有了这些信息就可以自动和 API 编排交互。\n",
    "\n",
    "RemoteToolkit 将从上述 URL 获取 OpenAPI.yaml 文件，并解析其中的输入和输出格式，然后提供给 Agent 进行交互。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 使用 RemoteToolkit 调用本地 RESTFul API 服务\n",
    "\n",
    "使用 EB Agent 调用本地 RESTFul API 服务只需要以下几行代码即可:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57207 - \"GET /.well-known/openapi.yaml HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:57208 - \"HEAD /.well-known/examples.yaml HTTP/1.1\" 404 Not Found\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Run][Start] FunctionAgent is about to start running with input:\n",
      "\u001b[94m添加一个单词“red”到我的单词本\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[94muser\u001b[92m \n",
      " content: \u001b[94m添加一个单词“red”到我的单词本\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " function_call: \u001b[93m\n",
      "{\n",
      "  \"name\": \"FastAPI/0.1.0/add_word_add_word_post\",\n",
      "  \"thoughts\": \"用户想要添加一个单词到单词本，我需要调用添加单词的工具完成此操作\",\n",
      "  \"arguments\": \"{\\\"word\\\":\\\"red\\\"}\"\n",
      "}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Tool][Start] \u001b[95mRemoteTool\u001b[92m is about to start running with input:\n",
      "\u001b[95m{\n",
      "  \"word\": \"red\"\n",
      "}\u001b[92m\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57213 - \"POST /add_word?version=0.1.0 HTTP/1.1\" 200 OK\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Tool][End] \u001b[95mRemoteTool\u001b[92m finished running with output:\n",
      "\u001b[95m{\n",
      "  \"message\": \"单词添加成功\",\n",
      "  \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[95mfunction\u001b[92m \n",
      " name: \u001b[95mFastAPI/0.1.0/add_word_add_word_post\u001b[92m \n",
      " content: \u001b[95m{\"message\": \"单词添加成功\", \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " content: \u001b[93m单词“red”已成功添加到您的单词本中。\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Run][End] FunctionAgent finished running.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "单词“red”已成功添加到您的单词本中。\n"
     ]
    }
   ],
   "source": [
    "from erniebot_agent.tools.remote_toolkit import RemoteToolkit\n",
    "from erniebot_agent.agents.function_agent import FunctionAgent\n",
    "from erniebot_agent.chat_models import ERNIEBot\n",
    "from erniebot_agent.memory import WholeMemory\n",
    "\n",
    "toolkit = RemoteToolkit.from_url(\"http://127.0.0.1:8020\")  # 必须存在：http://xxx.com/.well-known/openapi.yaml\n",
    "llm = ERNIEBot(\"ernie-3.5\")\n",
    "agent = FunctionAgent(llm, tools=toolkit.get_tools(), memory=WholeMemory())\n",
    "result = await agent.run(\"添加一个单词“red”到我的单词本\")\n",
    "print(result.text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Run][Start] FunctionAgent is about to start running with input:\n",
      "\u001b[94m单词本当中有哪些单词呢？\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[94muser\u001b[92m \n",
      " content: \u001b[94m单词本当中有哪些单词呢？\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " function_call: \u001b[93m\n",
      "{\n",
      "  \"name\": \"FastAPI/0.1.0/get_words_get_words_get\",\n",
      "  \"thoughts\": \"用户想要获取单词本中的内容\",\n",
      "  \"arguments\": \"{}\"\n",
      "}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Tool][Start] \u001b[95mRemoteTool\u001b[92m is about to start running with input:\n",
      "\u001b[95m{}\u001b[92m\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57221 - \"GET /get_words?version=0.1.0 HTTP/1.1\" 200 OK\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Tool][End] \u001b[95mRemoteTool\u001b[92m finished running with output:\n",
      "\u001b[95m{\n",
      "  \"words\": [\n",
      "    \"red\"\n",
      "  ],\n",
      "  \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[95mfunction\u001b[92m \n",
      " name: \u001b[95mFastAPI/0.1.0/get_words_get_words_get\u001b[92m \n",
      " content: \u001b[95m{\"words\": [\"red\"], \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " content: \u001b[93m单词本中的单词包括“red”。\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Run][End] FunctionAgent finished running.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "单词本中的单词包括“red”。\n"
     ]
    }
   ],
   "source": [
    "result = await agent.run(\"单词本当中有哪些单词呢？\")\n",
    "print(result.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 总结\n",
    "\n",
    "以上展示了如何启动一个本地 RESTFul API 服务 并在 ERNIE-Bot-Agent 中使用 RemoteTool调用，使用步骤和 LocalTool 一样。\n",
    "\n",
    "本地 RemoteTool Server 主要包含两部分：\n",
    "\n",
    "1. 本地 restful api 的服务：开发者可以使用 java、go 等其他变成语言开发服务，只需能正常通过 http 的方式调用即可。\n",
    "2. openapi.yaml 描述文件，主要是为了提供 API 的元信息。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Tool Server\n",
    "\n",
    "#### 介绍\n",
    "\n",
    "以上展示了如何在本地开发 RESTFul API并在 ERNIE-Bot-Agent 中使用，可这个通常是在现有的服务上调整的，如果想要从零开发一个 RESTFul API 的服务成本有点大，可通过ERNIE-Bot-Agent 中的 LocalTool 模块自定义本地 Tool，然后将其部署成服务即可。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 定义 LocalTool 集合\n",
    "\n",
    "以上述单词本的服务为例，接下来将会展示如何从零开发 LocalTool 并 serve 起来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any, Dict\n",
    "from erniebot_agent.tools.base import Tool, ToolParameterView\n",
    "\n",
    "prompt = '请避免使用\"根据提供的内容、文章、检索结果……\"等表述，不要做过多的解释。'\n",
    "\n",
    "\n",
    "# 这部分的代码完全可以复用\n",
    "class AddWordInput(ToolParameterView):\n",
    "    word: str = Field(description=\"待添加的单词\")\n",
    "\n",
    "\n",
    "wordbook = []\n",
    "\n",
    "\n",
    "class AddWordTool(Tool):\n",
    "    description: str = \"在单词本中添加一个单词\"\n",
    "    input_type = AddWordInput\n",
    "\n",
    "    async def __call__(self, word):\n",
    "        if word in wordbook:\n",
    "            return {\"message\": f\"单词：“{word}” 已存在\"}\n",
    "\n",
    "        wordbook.append(word)\n",
    "        return {\"message\": \"单词添加成功\", \"prompt\": prompt}\n",
    "\n",
    "\n",
    "class GetWordsTool(Tool):\n",
    "    description: str = \"获取单词本所有的单词\"\n",
    "\n",
    "    async def __call__(self):\n",
    "        return {\"words\": wordbook, \"prompt\": prompt}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以上针对于 `add_word`和`get_words` 分别转化成两个 Tool：`AddWordTool` 和 `GetWordsTool`。核心的功能模块代码一模一样，只是实现的形式不太一样。\n",
    "\n",
    "> 至于如何自定义 LocalTool 可参考：[自定义 LocalTool](../local_tool.ipynb)\n",
    "\n",
    "接下来将介绍如何使用 ToolManager 来 serve 一个工具集合："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 启动 Tool Server 服务"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Started server process [19881]\n",
      "INFO:     Waiting for application startup.\n",
      "INFO:     Application startup complete.\n",
      "INFO:     Uvicorn running on http://0.0.0.0:8021 (Press CTRL+C to quit)\n"
     ]
    }
   ],
   "source": [
    "from erniebot_agent.tools.tool_manager import ToolManager\n",
    "\n",
    "tool_manager = ToolManager([AddWordTool(), GetWordsTool()])\n",
    "\n",
    "thread = Thread(target=tool_manager.serve, args=(8021,))\n",
    "thread.daemon = True\n",
    "thread.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 执行 Agent\n",
    "\n",
    "以下将介绍：添加 red 单词到单词和检索单词本中的内容两个示例。\n",
    "\n",
    "* 添加单词"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57235 - \"GET /.well-known/openapi.yaml HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:57236 - \"HEAD /.well-known/examples.yaml HTTP/1.1\" 404 Not Found\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages/pydantic/json_schema.py:2080: PydanticJsonSchemaWarning: Default value <name: AddWordTool, description: 在单词本中添加一个单词> is not JSON serializable; excluding default from JSON schema [non-serializable-default]\n",
      "  warnings.warn(message, PydanticJsonSchemaWarning)\n",
      "/Users/wujingjing05/miniconda3/envs/eb-agent/lib/python3.10/site-packages/pydantic/json_schema.py:2080: PydanticJsonSchemaWarning: Default value <name: GetWordsTool, description: 获取单词本所有的单词> is not JSON serializable; excluding default from JSON schema [non-serializable-default]\n",
      "  warnings.warn(message, PydanticJsonSchemaWarning)\n",
      "\u001b[92mINFO - [Run][Start] FunctionAgent is about to start running with input:\n",
      "\u001b[94m添加一个单词“red”到我的单词本\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[94muser\u001b[92m \n",
      " content: \u001b[94m添加一个单词“red”到我的单词本\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " function_call: \u001b[93m\n",
      "{\n",
      "  \"name\": \"erniebot-agent-tools/0.0/AddWordTool\",\n",
      "  \"thoughts\": \"用户想要添加一个单词到单词本，我需要调用AddWordTool工具来实现这个需求。\",\n",
      "  \"arguments\": \"{\\\"word\\\":\\\"red\\\"}\"\n",
      "}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Tool][Start] \u001b[95mRemoteTool\u001b[92m is about to start running with input:\n",
      "\u001b[95m{\n",
      "  \"word\": \"red\"\n",
      "}\u001b[92m\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57242 - \"POST /erniebot-agent-tools/0.0/AddWordTool?version=0.0 HTTP/1.1\" 200 OK\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Tool][End] \u001b[95mRemoteTool\u001b[92m finished running with output:\n",
      "\u001b[95m{\n",
      "  \"message\": \"单词添加成功\",\n",
      "  \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[95mfunction\u001b[92m \n",
      " name: \u001b[95merniebot-agent-tools/0.0/AddWordTool\u001b[92m \n",
      " content: \u001b[95m{\"message\": \"单词添加成功\", \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " content: \u001b[93m单词“red”已成功添加到您的单词本中。\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Run][End] FunctionAgent finished running.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "单词“red”已成功添加到您的单词本中。\n"
     ]
    }
   ],
   "source": [
    "toolkit = RemoteToolkit.from_url(\"http://127.0.0.1:8021\")\n",
    "llm = ERNIEBot(\"ernie-3.5\")\n",
    "\n",
    "agent = FunctionAgent(llm, tools=toolkit.get_tools(), memory=WholeMemory())\n",
    "result = await agent.run(\"添加一个单词“red”到我的单词本\")\n",
    "print(result.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* 查询单词本中的所有单词"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Run][Start] FunctionAgent is about to start running with input:\n",
      "\u001b[94m单词本当中有哪些单词呢？\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[94muser\u001b[92m \n",
      " content: \u001b[94m单词本当中有哪些单词呢？\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " function_call: \u001b[93m\n",
      "{\n",
      "  \"name\": \"erniebot-agent-tools/0.0/GetWordsTool\",\n",
      "  \"thoughts\": \"用户想要获取单词本中的所有单词\",\n",
      "  \"arguments\": \"{}\"\n",
      "}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Tool][Start] \u001b[95mRemoteTool\u001b[92m is about to start running with input:\n",
      "\u001b[95m{}\u001b[92m\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:57254 - \"POST /erniebot-agent-tools/0.0/GetWordsTool?version=0.0 HTTP/1.1\" 200 OK\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Tool][End] \u001b[95mRemoteTool\u001b[92m finished running with output:\n",
      "\u001b[95m{\n",
      "  \"words\": [\n",
      "    \"red\"\n",
      "  ],\n",
      "  \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[95mfunction\u001b[92m \n",
      " name: \u001b[95merniebot-agent-tools/0.0/GetWordsTool\u001b[92m \n",
      " content: \u001b[95m{\"words\": [\"red\"], \"prompt\": \"请避免使用\\\"根据提供的内容、文章、检索结果……\\\"等表述，不要做过多的解释。\"}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " content: \u001b[93m单词本中的单词有：red。\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Run][End] FunctionAgent finished running.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "单词本中的单词有：red。\n"
     ]
    }
   ],
   "source": [
    "result = await agent.run(\"单词本当中有哪些单词呢？\")\n",
    "print(result.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 总结\n",
    "\n",
    "Tool Server 有如下优点：\n",
    "\n",
    "* 一套代码在本地和服务端都可以使用，ERNIE-Bot-Agent 也支持将开发的 tool 集合发布成 package 发布到 pypi 上提供给开发者使用。\n",
    "* 自动化生成 openapi.yaml 文件，不需要手动调整编写，极大程度上节省开发时间。\n",
    "* 代码界面简单，提升开发者的开发效率。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 使用 AI Studio 远程工具\n",
    "\n",
    "AI Studio 工具中心包含大量稳定服务，开发者可直接调用其工具实现自定义功能，比如以下调用百度翻译的远程工具，"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[92mINFO - [Run][Start] FunctionAgent is about to start running with input:\n",
      "\u001b[94m“我明天出去玩”这句话合规吗？\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[94muser\u001b[92m \n",
      " content: \u001b[94m“我明天出去玩”这句话合规吗？\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " function_call: \u001b[93m\n",
      "{\n",
      "  \"name\": \"text-moderation/v1.2/text_moderation\",\n",
      "  \"thoughts\": \"用户想要知道“我明天出去玩”这句话是否合规。这需要审核文本的合规性。\",\n",
      "  \"arguments\": \"{\\\"text\\\":\\\"我明天出去玩\\\"}\"\n",
      "}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Tool][Start] \u001b[95mRemoteTool\u001b[92m is about to start running with input:\n",
      "\u001b[95m{\n",
      "  \"text\": \"我明天出去玩\"\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [Tool][End] \u001b[95mRemoteTool\u001b[92m finished running with output:\n",
      "\u001b[95m{\n",
      "  \"conclusion\": \"合规\",\n",
      "  \"isHitMd5\": false,\n",
      "  \"conclusionType\": 1\n",
      "}\u001b[92m\u001b[0m\n",
      "\u001b[92mINFO - [LLM][Start] ERNIEBot is about to start running with input:\n",
      " role: \u001b[95mfunction\u001b[92m \n",
      " name: \u001b[95mtext-moderation/v1.2/text_moderation\u001b[92m \n",
      " content: \u001b[95m{\"conclusion\": \"合规\", \"isHitMd5\": false, \"conclusionType\": 1}\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [LLM][End] ERNIEBot finished running with output:\n",
      " role: \u001b[93massistant\u001b[92m \n",
      " content: \u001b[93m根据工具审核结果，“我明天出去玩”这句话是合规的。\u001b[92m \u001b[0m\n",
      "\u001b[92mINFO - [Run][End] FunctionAgent finished running.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "根据工具审核结果，“我明天出去玩”这句话是合规的。\n"
     ]
    }
   ],
   "source": [
    "toolkit = RemoteToolkit.from_aistudio(\"text-moderation\")\n",
    "agent = FunctionAgent(llm=ERNIEBot(model=\"ernie-3.5\"), tools=toolkit.get_tools())\n",
    "result = await agent.run(\"“我明天出去玩”这句话合规吗？\")\n",
    "print(result.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RemoteTool vs RemoteToolkit\n",
    "\n",
    "RemoteTool 是单个远程工具，比如添加单词到单词本功能属于单个 RemoteTool，可是：添加单词、删除单词和查询单词这几个功能组装在一起就组成了一个 Toolkit（工具箱），故称为 RemoteToolkit。\n",
    "\n",
    "以下将会统一使用 RemoteTool 来标识远程工具。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RemoteTool 如何与 Agent 交互\n",
    "\n",
    "无论是 LocalTool 还是 RemoteTool 都必须要提供核心的信息：\n",
    "\n",
    "* tool 的描述信息\n",
    "* tool 的输入和输出参数\n",
    "* tool 的执行示例\n",
    "\n",
    "LocalTool 是通过代码定义上述信息，而 RemoteTool 则是通过`openapi.yaml`来定义上述信息，RemoteToolkit 在加载时将会解析`openapi.yaml`中的信息，并在执行时将对应 Tool 的元信息传入 Agent LLM 当中。\n",
    "\n",
    "此外 RemoteTool 的远端调用是通过 http 的方式执行，同时遵照 [OpenAPI 3.0](https://swagger.io/specification/) 的规范发送请求并解析响应。OpenAPI.yaml 文件示例如下所示："
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "paddlenlp",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
